package com.zhx.common.websocket.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.zhx.common.common.config.ThreadPoolConfig;
import com.zhx.common.common.event.UserOfflineEvent;
import com.zhx.common.common.event.UserOnlineEvent;
import com.zhx.common.user.dao.UserDao;
import com.zhx.common.user.domain.entity.User;
import com.zhx.common.user.domain.enums.RoleEnum;
import com.zhx.common.user.service.IRoleService;
import com.zhx.common.user.service.LoginService;
import com.zhx.common.user.service.adapter.WSAdapter;
import com.zhx.common.user.service.cache.UserCache;
import com.zhx.common.websocket.domain.dto.WSChannelExtraDTO;
import com.zhx.common.websocket.domain.vo.resp.WSBaseResp;
import com.zhx.common.websocket.service.WebSocketService;
import com.zhx.common.websocket.service.adapter.WebSocketAdapter;
import com.zhx.common.websocket.util.NettyUtil;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.util.Optionals;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;


/**
 * 专门管理webSocket的逻辑，包括推拉
 */

@Service
@Slf4j
public class WebSocketServiceImpl implements WebSocketService {

    @Resource
    @Lazy
    private WxMpService wxMpService;

    @Resource
    private LoginService loginService;

    @Resource
    private UserDao userDao;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Resource
    private IRoleService roleService;

    @Resource
    private UserCache userCache;

    /**
     * 当Spring容器中有多个相同类型的Bean时，可以使用 @Qualifier 注解来指定需要注入哪一个Bean。
     */
    @Resource()
    @Qualifier(ThreadPoolConfig.WS_EXECUTOR)
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;


    /**
     * 管理所有在线用户的连接（包括登陆态和游客）,用户登录成功后，会绑定channel和用户uid
     * 用户进入网页还没有登陆时value为空
     */
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MP = new ConcurrentHashMap<>();

    /**
     * 所有在线的用户和对应的socket
     * 一个用户可能会有多个连接（多端？）
     */
    private static final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> ONLINE_UID_MAP = new ConcurrentHashMap<>();

    public static final int MAXIMUM_SIZE = 1000;

    //Duration.ofHours(1)是一个静态方法，它创建了一个表示持续时间为1小时的Duration对象。
    public static final Duration DURATION = Duration.ofHours(1);

    /**
     * 临时保存登陆code和channel的映射关系
     * 1.用户请求二维码 刚生成code 的时候添加
     * 2.登录成功后会删除
     */
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUM_SIZE)  //最大容量
            .expireAfterWrite(DURATION) //过期时间
            .build();

    @Override
    public void connect(Channel channel) {
        //未登录时value为null，这个WSChannelExtraDTO是用户的uid
        ONLINE_WS_MP.put(channel, new WSChannelExtraDTO());
    }

    /**
     * 处理登录请求，生成随机码并推送带参二维码给前端。
     *
     * @param channel 与客户端建立的通道，用于后续消息推送
     */
    @Override
    public void handleLoginReq(Channel channel) {
        // 生成随机码并保存code和channel的关系
        Integer code = generateLoginCode(channel);

        // 向微信申请随机带参二维码
        WxMpQrCodeTicket wxMpQrCodeTicket;
        try {
            wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) DURATION.getSeconds());
        } catch (WxErrorException e) {
            // 微信接口调用异常处理
            throw new RuntimeException(e);
        }

        // 将二维码URL推送给前端
        sendMsg(channel, WebSocketAdapter.buildResp(wxMpQrCodeTicket));
    }



    @Override
    public void scanLoginSuccess(Integer code, Long id) {
        //确认连接在机器上
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(code);
        //如果不存在
        if(Objects.isNull(channel)){
            return;
        }
        User user = userDao.getById(id);
        //移除code
        WAIT_LOGIN_MAP.invalidate(code);
        //调用登录模块获取token
        String token = loginService.login(id);
        //用户登录成功
        loginSuccess(channel,user,token);
    }


    @Override
    public void authorize(Channel channel, String token) {
        Long validUid = loginService.getValidUid(token);

        if(Objects.nonNull(validUid)){
            User user = userDao.getById(validUid);
            loginSuccess(channel,user,token);
        }else{ //为空
            sendMsg(channel,WebSocketAdapter.buildInvalidTokenResp());
        }
    }


    @Override
    public void sendToAllOnline(WSBaseResp<?> resp, Long skipUid) {
        ONLINE_WS_MP.forEach((channel,ext)->{
            if(Objects.nonNull(skipUid) && Objects.equals(ext.getUid(),skipUid)){
                return;
            }
            threadPoolTaskExecutor.execute(()->sendMsg(channel,resp));
        });
    }

    @Override
    public void sendToAllOnline(WSBaseResp<?> wsBaseResp) {
        sendToAllOnline(wsBaseResp,null);
    }

    /**
     * 向指定的用户UID发送消息。
     * @param wsBaseResp 要发送的消息对象，继承自WSBaseResp。
     * @param uid 用户的唯一标识符。
     * 方法会尝试查找在线用户，如果用户在线，则将消息发送给该用户的所有连接通道。
     */
    @Override
    public void sendToUid(WSBaseResp<?> wsBaseResp, Long uid) {
        // 从在线用户映射中获取指定UID的用户频道列表
        CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid);
        // 如果频道列表为空，表示用户不在线，记录日志并返回
        if(CollUtil.isEmpty(channels)){
            log.info("用户：{}不在线",uid);
            return;
        }
        // 遍历用户的所有频道，使用线程池异步发送消息
        channels.forEach(channel -> {
            threadPoolTaskExecutor.execute(()->sendMsg(channel,wsBaseResp));
        });
    }

    /**
     * 从在线用户列表中移除指定的Channel。
     * 当成功移除用户且该用户是所有连接的最后一条时，发布用户下线事件。
     *
     * @param channel 表示用户连接的Channel，用于标识用户。
     */
    @Override
    public void remove(Channel channel) {
        // 根据Channel从在线用户映射中获取用户扩展信息
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MP.get(channel);
        // 尝试获取用户ID
        Optional<Long> uidOptional = Optional.ofNullable(wsChannelExtraDTO).map(WSChannelExtraDTO::getUid);
        // 尝试将用户标记为离线，并检查是否成功且该用户没有其他连接
        boolean offlineAll = offline(channel,uidOptional);
        // 如果用户ID存在且所有连接都已关闭，则发布用户下线事件
        if(uidOptional.isPresent() && offlineAll){
            // 创建用户实例，设置用户ID和最后操作时间
            User user = new User();
            user.setId(uidOptional.get());
            user.setLastOptTime(new Date());
            // 发布用户下线事件
            applicationEventPublisher.publishEvent(new UserOfflineEvent(this,user));
        }
    }

    @Override
    public Boolean scanSuccess(Integer loginCode) {
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(loginCode);
        if (Objects.nonNull(channel)) {
            sendMsg(channel, WSAdapter.buildScanSuccessResp());
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }


    /**
     * 将指定的频道标记为离线状态。
     * 如果提供了用户ID，并且该用户ID在系统中存在对应的频道，则从相应的用户频道列表中移除该频道。
     * 当用户ID存在且移除频道后，如果该用户没有其他在线频道，则返回true，表示该用户已完全离线。
     * 如果没有提供用户ID，或者用户ID不存在对应的频道，则直接返回true。
     *
     * @param channel 要标记为离线的频道对象。
     * @param uidOptional 可选的用户ID，如果存在，用于从用户频道映射中移除指定频道。
     * @return 如果用户完全离线（即没有其他在线频道）或未找到对应的频道，返回true；否则返回false。
     */
    private boolean offline(Channel channel, Optional<Long> uidOptional) {
        // 从在线WebSocket映射中移除指定的频道
        ONLINE_WS_MP.remove(channel);
        if(uidOptional.isPresent()){
            // 获取指定用户ID对应的频道列表
            CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uidOptional.get());
            if(CollUtil.isNotEmpty(channels)){
                // 从用户频道列表中移除指定的频道
                channels.removeIf(ch -> Objects.equals(ch, channel));
            }
            // 检查移除频道后，用户是否还有其他在线频道
            return CollUtil.isEmpty(ONLINE_UID_MAP.get(uidOptional.get()));
        }
        // 如果没有提供用户ID，或用户ID不存在映射中，直接返回true
        return true;
    }

    //用户登陆成功
    private void loginSuccess(Channel channel, User user, String token) {
        System.out.println("用户登陆成功");

        online(channel, user.getId());

        //推送成功消息（携带用户信息）给前端 还需要判断权限
        sendMsg(channel,WebSocketAdapter.buildResp(user,token,roleService.hasPower(user.getId(), RoleEnum.CHAT_MANAGE)));

        //判断用户是否在线
        boolean online = userCache.isOnline(user.getId());
        if(!online){
            //设置用户上线时间
            user.setLastOptTime(new Date());
            //更新ip
            user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
            //用户上线成功的事件 发布事件
            applicationEventPublisher.publishEvent(new UserOnlineEvent(this,user));
        }
    }

    private static void online(Channel channel, Long uid) {
        //保存channel的对应id
        WSChannelExtraDTO wsChannelExtraDTO = ONLINE_WS_MP.get(channel);
        wsChannelExtraDTO.setUid(uid);

        ONLINE_UID_MAP.putIfAbsent(uid,new CopyOnWriteArrayList<>());
        ONLINE_UID_MAP.get(uid).add(channel);
        NettyUtil.setAttr(channel,NettyUtil.UID, uid);
    }

    private void sendMsg(Channel channel, WSBaseResp<?> resp) {
        //将响应对象resp转换成JSON格式的字符串，然后创建一个新的文本WebSocket帧，并将其发送到网络连接，且立即刷新发送缓冲区。
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(resp)));
    }

    private Integer generateLoginCode(Channel channel) {
        //转为map，如果map中不存在则放入，成功后返回null，不为null的话重新生成
        Integer code;
        do {
            code = RandomUtil.randomInt(99999999);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }
}
